package ro.flashbite.console
{
	import fl.controls.Button;
	import fl.controls.ComboBox;
	import fl.controls.UIScrollBar;
	import fl.data.DataProvider;
	import fl.events.ListEvent;
	
	import flash.display.*;
	import flash.events.EventDispatcher;
	import flash.events.MouseEvent;
	import flash.events.TouchEvent;
	import flash.geom.Point;
	import flash.system.System;
	import flash.text.TextField;
	import flash.ui.Multitouch;
	import flash.utils.Dictionary;
	
	import ro.flashbite.helpers.DrawingHelpers;
	import ro.flashbite.helpers.MathHelpers;
	import ro.flashbite.interfaces.*;
	import ro.flashbite.loggers.ILogger;
	import ro.flashbite.loggers.LoggerFactory;
	import ro.flashbite.utils.framer.FrameBeacon;
	import ro.flashbite.utils.framer.ITickUpdater;
	
	/**
	 * Console for showing total memory, fps, outputs from loggers
	 * 
	 * v 1.0
	 * 
	 * @author Adrian Barbu
	 */
	public final class Console extends EventDispatcher implements IDestroyable, IFunctionBindable, IOutputable, IResizeable, IInteractive
	{
		private var _stage:Stage;
		
		private var _logger:ILogger;
		
		private var _touchEnabled:Boolean;
		
		private var _levelList:Array;
		private var _levelListCombo:ComboBox;
		
		private var _bindingsDatabase:Dictionary;
		private var _bindingsButtons:Vector.<Button>;
		
		private var _minimizeBtn:Button;
		private var _maximizeBtn:Button;
		private var _closeBtn:Button;
		
		private var _sleepBtn:Button;
		private var _wakeupBtn:Button;
		private var _destroyBtn:Button;
		private var _gcBtn:Button;
		
		private var _allButtons:Vector.<Button>;
		
		private var _isMinimized:Boolean;
		
		private var _fpsTField:TextField;
		private var _memoryTField:TextField;
		
		private var _outputTField:TextField;
		private var _outputScrollBar:UIScrollBar;
		private var _linesCount:int;
		
		private var _container:Sprite;
		
		private var _topBarContainer:Sprite;
		
		private var _frameBeacon:ITickUpdater;
		
		private static const UPDATE_COUNTER:uint = 10;
		private var _countUpdate:uint = 0;
		
		private var _averageTimePerFrame:Number;
		
		// ================================================================================================================================
		// CONSTRUCTOR
		// ================================================================================================================================
		
		public function Console(stage:Stage, levelList:Array = null)
		{
			_constructor(stage, levelList);
		}
		private function _constructor(stage:Stage, levelList:Array):void
		{
			_stage = stage;
			_levelList = levelList;
			
			//internals
			_logger = LoggerFactory.getLogger("Console");
			_touchEnabled = Multitouch.supportsTouchEvents;
			_bindingsDatabase = new Dictionary(true);
			
			_allButtons = new Vector.<Button>();
			_bindingsButtons = new Vector.<Button>();
			
			_container = new Sprite();
			_stage.addChild(_container);
			
			_topBarContainer = new Sprite();
			_container.addChild(_topBarContainer);
			
			_linesCount = -1;
			
			draw(_stage.stageWidth, true);
			minimize();
			
			_allButtons.push(_sleepBtn, _wakeupBtn, _destroyBtn, _gcBtn, _minimizeBtn, _maximizeBtn, _closeBtn);
			
			_countUpdate = 0;
			
			_frameBeacon = FrameBeacon.getInstance();
			_frameBeacon.registerCallback(onFrameBeaconTick);
			
			_averageTimePerFrame = 0;
		}
		
		// ================================================================================================================================
		// DESTRUCTOR
		// ================================================================================================================================
		
		public function destroy():void
		{
			if (_logger) {
				LoggerFactory.clearLogger(_logger.name);
				_logger = null;
			}
			
			if (_frameBeacon) {
				_frameBeacon.unregisterCallback(onFrameBeaconTick);
				_frameBeacon = null;
			}
			
			removeAllBindings();
			
			_stage = null;
			_levelList = null;
			_levelListCombo = null;
			_bindingsDatabase = null;
			_bindingsButtons = null
			_minimizeBtn = null;
			_maximizeBtn = null;
			_closeBtn = null;
			_sleepBtn = null;
			_wakeupBtn = null;
			_destroyBtn = null;
			_gcBtn = null
			_allButtons = null;
			_fpsTField = null;
			_memoryTField = null;
			_outputTField = null;
			_outputScrollBar = null;
			if (_topBarContainer) {
				while (_topBarContainer.numChildren) _topBarContainer.removeChildAt(0);
				if (_topBarContainer.parent) _topBarContainer.parent.removeChild(_topBarContainer);
				_topBarContainer = null;
			}
			if (_container) {
				while (_container.numChildren) _container.removeChildAt(0);
				if (_container.parent) _container.parent.removeChild(_container);
				_container = null;
			}
		}
		
		// ================================================================================================================================
		// PUBLIC
		// ================================================================================================================================
		
		// --------------------------------------------------------------------------------------------------------------------------------
		// IFunctionBindable
		public function addBinding(bindingID:String, bindingFunction:Function, bindingParam:Array = null, overwriteSame:Boolean = true):void
		{
			//put it in database
			if (_bindingsDatabase[bindingID] && overwriteSame) {
				removeBinding(bindingID);
				//redraw
				redrawBindButtons();
			} else {
				//save it
				_bindingsDatabase[bindingID] = new BindVO(bindingFunction, bindingParam);
				//create btn
				var newBindBtn:Button = DrawingHelpers.createBasicButton(ConsoleConsts.BIND_BTN_WIDTH, ConsoleConsts.BTN_HEIGHT, ConsoleConsts.TF_ALL_SMALL, bindingID);
				//put it in vector
				_bindingsButtons.push(newBindBtn);
				//redraw
				redrawBindButtons();
				//enable-it
				disable(); enable();
 			}
		}
		
		public function removeBinding(bindingID:String):void
		{
			if (_bindingsDatabase[bindingID]) {
				//remove from database
				(_bindingsDatabase[bindingID] as BindVO).destroy();
				delete _bindingsDatabase[bindingID];
				//remove it from view + remove interaction
				removeBindBtnByName(bindingID);
			}
		}
		private function removeBindBtnByName(name:String):void
		{
			var lenBind:uint = _bindingsButtons.length,
				i:uint = 0,
				btn:Button;
			for (i = 0; i < lenBind; i++) {
				btn = _bindingsButtons[i] as Button;
				if (btn.name == name) {
					_bindingsButtons.splice(i, 1);
					disable(); enable();
					break;
				}
			}
		}
		// --------------------------------------------------------------------------------------------------------------------------------
		
		// --------------------------------------------------------------------------------------------------------------------------------
		// IOutputable
		public function appendText(text:String, textColor:uint):void
		{
			_linesCount++;
			
			//adds text to outputTextField
			_outputTField.appendText(_linesCount + " " + text + "\n");
			//update scrollBar
			_outputScrollBar.update();
			_outputScrollBar.drawNow();
			
			//set textFormat accordly
			var startNewTextLocation:int = _outputTField.text.length - text.length;
			switch (textColor) {
				case ConsoleConsts.TF_ALL.color:
					_outputTField.setTextFormat(ConsoleConsts.TF_ALL,   startNewTextLocation, _outputTField.text.length);
					break;
				case ConsoleConsts.TF_DEBUG.color:
					_outputTField.setTextFormat(ConsoleConsts.TF_DEBUG, startNewTextLocation, _outputTField.text.length); 
					break;
				case ConsoleConsts.TF_ERROR.color:
					_outputTField.setTextFormat(ConsoleConsts.TF_ERROR, startNewTextLocation, _outputTField.text.length);
					break;		
			}
		}
		// --------------------------------------------------------------------------------------------------------------------------------
		
		// --------------------------------------------------------------------------------------------------------------------------------
		// IResizeable
		public function resize(newWidth:Number, newHeight:Number):void
		{
			draw(newWidth, false);
		}
		// --------------------------------------------------------------------------------------------------------------------------------
		
		// --------------------------------------------------------------------------------------------------------------------------------
		// IInteractive
		public function enable():void
		{
			setInteraction(true);
		}
		public function disable():void
		{
			setInteraction(false);
		}
		// --------------------------------------------------------------------------------------------------------------------------------
		
		
		/**
		 * Starts the frameBeacon and interaction on buttons + level list combo
		 */		
		public function start():void
		{	
			_logger.debug("Touch enabled : " + _touchEnabled);
			
			enable();
		}
		
		/**
		 * Closes the console, but it will still be active 
		 */		
		public function close():void
		{
			//visibility false
			disable();
			if (_container) {
				_container.visible = false;
			}
		}
		
		/**
		 * Clean-up the internal database of bindings 
		 */		
		public function removeAllBindings():void
		{
			for (var bindingID:String in _bindingsDatabase) {
				removeBinding(bindingID);
			}
		}
		
		// ================================================================================================================================
		// PRIVATE
		// ================================================================================================================================
		
		/**
		 * Add or remove interaction on console + binding buttons
		 * 
		 * @param enable 
		 */		
		private function setInteraction(enable:Boolean = true):void
		{
			var eventType:String,
				func:Function,
				btn:Button;
			
			if (_touchEnabled) {
				eventType = TouchEvent.TOUCH_TAP;
				func = onButtonTouchTap;
			} else {
				eventType = MouseEvent.CLICK;
				func = onButtonClick;
			}
			//console btns
			for each (btn in _allButtons) {
				setInteractionOnBtn(btn, eventType, func, enable);
			}
			//binding buttons
			for each (btn in _bindingsButtons) {
				setInteractionOnBtn(btn, eventType, func, enable);
			}
			//combo list -> same event on mouse and touch
			if (_levelListCombo) {
				if (enable) {
					_levelListCombo.dropdown.addEventListener(ListEvent.ITEM_CLICK, onComboItemClick, false, 0, true);
				} else _levelListCombo.dropdown.removeEventListener(ListEvent.ITEM_CLICK, onComboItemClick);
			}
			
			//interaction on parent container
			if (_container) {
				_container.mouseEnabled = enable; 
			}
		}
		private function setInteractionOnBtn(btn:Button, eventType:String, func:Function, enable:Boolean = true):void
		{
			if (btn) {
				if (enable) {
					btn.addEventListener(eventType, func, false, 0, true);
				} else btn.removeEventListener(eventType, func);
			}
		}
		
		/**
		 * On enter frame (or Timer), the FrameBeacon will dispatch event; In this function will be recalculated fps and memory allocation
		 * 
		 * @param timePerFrame : (uint) time passed in ms till last enterFrame
		 */		
		private function onFrameBeaconTick(timePerFrame:Number):void
		{
			_averageTimePerFrame += timePerFrame;
			
			//show fps and memory
			if (++_countUpdate % UPDATE_COUNTER == 0) {
				//_logger.debug("Enter frame FPS....");
				//calculate average time per frame
				_averageTimePerFrame = _averageTimePerFrame / UPDATE_COUNTER;
				//write fps
				var fps:Number = MathHelpers.roundToDecimals(1 / _averageTimePerFrame, 2);
				_fpsTField.text = "FPS : " + fps;
				//write mem ocuppation
				_memoryTField.text = "MEM : " + MathHelpers.roundToDecimals(MathHelpers.bytesToKb(System.totalMemoryNumber)/1024, 3) + " MB";
				//reset average time
				_averageTimePerFrame = 0;
			}
		}
		
		/**
		 * Hide the output textField
		 */		
		private function minimize():void
		{
			//hide output + hide minimize btn + show maximize btn
			_outputTField.visible = false;
			_outputScrollBar.visible = false;
			_maximizeBtn.visible = true; 
			_minimizeBtn.visible = false;
		}
		
		/** 
		 * Show the output textField
		 */		
		private function maximize():void
		{
			//show output + hide maximize btn + show minimize btn
			_outputTField.visible = true;
			_outputScrollBar.visible = true;
			_maximizeBtn.visible = false; _minimizeBtn.visible = true;
		}
		
		/**
		 * If the console has a combo with all level id's, can dispatch a click event so that the Engine 
		 * will make the swap/loading of the new level
		 * 
		 * @param e : (ListEvent) the ListEvent.ITEM_CLICK event triggered by item in combo list; also, the same event is triggered on touchTap
		 */		
		private function onComboItemClick(e:ListEvent):void
		{
			var lvlID:String = e.item.classID;
			_logger.debug("Click on item with id = " + lvlID);
			dispatchEvent(new ConsoleEvent(ConsoleEvent.COMBO_LEVEL_CLICK, lvlID));
		}
		
		/**
		 * All interaction on buttons will be received here; The targets will be 'switched' by name
		 * 
		 * @param e : (MouseEvent) the MouseEvent.CLICK event triggered by interactive object
		 * @param e : (TouchEvent) the TouchEvent.TOUCH_TAP event triggered by interactive object
		 */		
		private function onButtonClick(e:MouseEvent):void 	 { onBtnClickOrTouchInteraction(e.currentTarget as Button); }
		private function onButtonTouchTap(e:TouchEvent):void { onBtnClickOrTouchInteraction(e.currentTarget as Button); }
		private function onBtnClickOrTouchInteraction(target:Button):void
		{
			_logger.debug("onBtnClickOrTouchInteraction() targetName  : " + ((target) ? target.name : " NULL!!!"));
			
			if (target) {
				var event:ConsoleEvent,
					targetName:String = target.name;
				switch (targetName) {
					case "SLEEP":
						event = new ConsoleEvent(ConsoleEvent.SLEEP);
						break;
					case "WAKEUP":
						event = new ConsoleEvent(ConsoleEvent.WAKEUP);
						break;
					case "DESTROY":
						event = new ConsoleEvent(ConsoleEvent.DESTROY);
						break;
					case "GC":
						System.gc();
						break;
					case "CLOSE":
						this.close();
						event = new ConsoleEvent(ConsoleEvent.CONSOLE_CLOSE);
						break
					case "MINIMIZE":
						this.minimize();
						break;
					case "MAXIMIZE":
						this.maximize();
						break;
					default:
						var bindVo:BindVO = _bindingsDatabase[targetName];
						if (bindVo) {
							bindVo.func.apply(null, bindVo.params);
						} else {
							_logger.error("This target name '" + targetName + "' doesn't have interaction handled");
						}
						break;
				}
			}
			
			if (event) {
				this.dispatchEvent(event);
				event.destroy();
			}
		}
		
		/**
		 * Function to draw/create all buttons and levelListCombo(if exists)
		 * 
		 * @param widthToFit : (Number) the dimension in width were the console must fit; If the dimension is smaller than 
		 * 					   the minimum dimension of console, the width will be that value 
		 * @param firstTime : (Boolean) true if all items will be drawed, false if only repositioning of them
		 */		
		private function draw(widthToFit:Number, firstTime:Boolean = true):void
		{
			//set the minumum dimension in width of top container
			var minWith:Number = ConsoleConsts.MIN_WIDTH;
			if (_levelList) {
				minWith += ConsoleConsts.WIDTH_COMBO_LVL_LIST + ConsoleConsts.BUTTONS_OFFSET;
			}
			if (_bindingsButtons.length) {
				minWith += (ConsoleConsts.WIDTH_BUTTON_BINDING + ConsoleConsts.BUTTONS_OFFSET) * _bindingsButtons.length;
			}
			var realWidth:Number = Math.max(widthToFit, minWith);
			
			//draw the top container
			if (realWidth != _topBarContainer.width) {
				_topBarContainer.graphics.clear();
				_topBarContainer.graphics.beginFill(0xCCCCCC, .5);
				_topBarContainer.graphics.drawRect(0, 0, realWidth, ConsoleConsts.TOPBAR_HEIGHT);
				_topBarContainer.graphics.endFill();
			}
			
			//draw fps and memory tFields
			if (_fpsTField == null) { //only one check; static location of item, so no need to re-arrange items
				_fpsTField = 	DrawingHelpers.createBasicTextField(90, 20, ConsoleConsts.TF_ALL_SMALL, "fps", "FPS : ", 	new Point(2,7),   _topBarContainer);
				_fpsTField.border = true;
				_memoryTField = DrawingHelpers.createBasicTextField(120, 20, ConsoleConsts.TF_ALL_SMALL, "mem", "| MEM : ", new Point(100,7), _topBarContainer);
				_memoryTField.border = true;
			}
			
			//draw sleep, wakeup, destroy, gc buttons
			if (_sleepBtn == null) {
				_sleepBtn = 	DrawingHelpers.createBasicButton(75, ConsoleConsts.BTN_HEIGHT, ConsoleConsts.TF_ALL_SMALL, "SLEEP",   new Point(_memoryTField.x + _memoryTField.width + ConsoleConsts.BUTTONS_SPACING, 1), _topBarContainer);
				_wakeupBtn = 	DrawingHelpers.createBasicButton(75, ConsoleConsts.BTN_HEIGHT, ConsoleConsts.TF_ALL_SMALL, "WAKEUP",  new Point(_sleepBtn.x + _sleepBtn.width + ConsoleConsts.BUTTONS_SPACING, 		 1), _topBarContainer);
				_destroyBtn = 	DrawingHelpers.createBasicButton(75, ConsoleConsts.BTN_HEIGHT, ConsoleConsts.TF_ALL_SMALL, "DESTROY", new Point(_wakeupBtn.x + _wakeupBtn.width + ConsoleConsts.BUTTONS_SPACING, 		 1), _topBarContainer);
				_gcBtn = 		DrawingHelpers.createBasicButton(40, ConsoleConsts.BTN_HEIGHT, ConsoleConsts.TF_ALL_SMALL, "GC", 	  new Point(_destroyBtn.x + _destroyBtn.width + ConsoleConsts.BUTTONS_SPACING, 	 1), _topBarContainer);
			}
			
			//draw global buttons
			if (_closeBtn == null) {
				_closeBtn = DrawingHelpers.createBasicButton(ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.TF_ALL_SMALL, "CLOSE", new Point(realWidth - ConsoleConsts.MIN_MAX_BTN_WIDTH, 0), _topBarContainer);
				drawOnButton(_closeBtn, false, false, true);
			} else _closeBtn.x = realWidth - _closeBtn.width;
			if (_minimizeBtn == null) {
				//create here both minimize and maximize buttons
				_minimizeBtn = DrawingHelpers.createBasicButton(ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.TF_ALL_SMALL, "MINIMIZE", new Point(_closeBtn.x - ConsoleConsts.MIN_MAX_BTN_WIDTH - ConsoleConsts.SPACE_CLOSE_MINMAX, 0), _topBarContainer);
				drawOnButton(_minimizeBtn, true, false, false);
				_maximizeBtn = DrawingHelpers.createBasicButton(ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.MIN_MAX_BTN_WIDTH, ConsoleConsts.TF_ALL_SMALL, "MAXIMIZE", new Point(_minimizeBtn.x, 0), _topBarContainer);
				drawOnButton(_maximizeBtn, false, true, false);
			} else {
				_minimizeBtn.x = _closeBtn.x - ConsoleConsts.MIN_MAX_BTN_WIDTH - ConsoleConsts.SPACE_CLOSE_MINMAX;
			 	_maximizeBtn.x = _minimizeBtn.x;
			}
			
			//draw output textField
			if (_outputTField == null) {
				//create it + create scrollbar
				_outputTField = DrawingHelpers.createTextFieldLikeTextArea(realWidth - ConsoleConsts.SCROLLBAR_WIDTH, ConsoleConsts.OUTPUT_HEIGHT, ConsoleConsts.TF_ALL, "output", "", new Point(0, ConsoleConsts.TOPBAR_HEIGHT), _topBarContainer);
				_outputScrollBar = DrawingHelpers.createScrollBarAndAttachToTextField(_outputTField, _topBarContainer);
			} else {
				_outputTField.width = realWidth;
				_outputScrollBar.x = realWidth - ConsoleConsts.SCROLLBAR_WIDTH;
			}
			_outputScrollBar.update();
			_outputScrollBar.drawNow();
			
			//draw _levelListCombo
			if (_levelList) {
				if (_levelListCombo == null) {
					//create combo-box;
					_levelListCombo = new ComboBox();
					_levelListCombo.dataProvider = new DataProvider(_levelList);
					_levelListCombo.labelField = "classID";
					//add it on console sprite + positioning
					_topBarContainer.addChild(_levelListCombo);
					_levelListCombo.x = _gcBtn.x + _gcBtn.width + 3 * ConsoleConsts.BUTTONS_SPACING; _levelListCombo.y = _gcBtn.y; 
					_levelListCombo.width = 70; _levelListCombo.height = ConsoleConsts.BTN_HEIGHT;
				}
			}
			
			//draw bindings buttons:
			redrawBindButtons();
		}
		
		/**
		 * Remove and re-add all bindings to container, setting also locati
		 */		
		private function redrawBindButtons():void
		{
			var lenBind:uint = _bindingsButtons.length;
			if (lenBind > 0) {
				var leftBindX:Number = _levelListCombo ? (_levelListCombo.x + _levelListCombo.width + 3 * ConsoleConsts.BUTTONS_SPACING) : (_gcBtn.x + _gcBtn.width + ConsoleConsts.BUTTONS_SPACING),
					rightBindX:Number = _minimizeBtn.x,
					isPlaceToPutOneBind:Boolean,
					i:uint,
					btn:Button;
				for (i = 0; i < lenBind; i++) {
					isPlaceToPutOneBind = Boolean((rightBindX - leftBindX) > ConsoleConsts.BIND_BTN_WIDTH + ConsoleConsts.BUTTONS_SPACING);
					btn = _bindingsButtons[i] as Button;
					if (isPlaceToPutOneBind) {
						//set new x and y to btn; put button
						btn.y = 1; btn.x = leftBindX;
						_topBarContainer.addChild(btn);
						//save new leftBindX
						leftBindX += ConsoleConsts.BIND_BTN_WIDTH + ConsoleConsts.BUTTONS_SPACING;
					} else {
						//remove btn from parent
						if (btn.parent) btn.parent.removeChild(btn);
					}
				}
			}
		}
		
		/**
		 * Draws a specific graphics on button so the button look like a minimize, maximimize or close window button
		 *  
		 * @param button : (Button) instance to draw graphics
		 * @param minimize : (Boolean) if true, draws a underline like graphics
		 * @param maximize : (Boolean) if true, draws a box like graphics
		 * @param close : (Boolean) if true, draws a X like graphics
		 */		
		private function drawOnButton(button:Button, minimize:Boolean = true, maximize:Boolean = false, close:Boolean = false):void
		{
			button.label = "";
			button.graphics.clear();
			button.graphics.lineStyle(0, 0xFFFFFF, 1, true);
			button.graphics.beginFill(0xFFFFFF);
			const offset:Number = 1;
			button.graphics.drawRoundRect(offset, offset, button.width - offset * 2, button.height - offset * 2, 2.5, 2.5);
			button.graphics.endFill();
			
			button.graphics.lineStyle(4, 0x000000, 1, true, LineScaleMode.NONE, CapsStyle.SQUARE, JointStyle.MITER, 2);
			
			const margin_px:Number = 4,
				  pX:Number = button.width - margin_px;;
			if (minimize) {
				button.graphics.moveTo(margin_px,pX);
				button.graphics.lineTo(pX,pX);
			} else if (maximize) {
				button.graphics.drawRect(margin_px,margin_px,pX - margin_px,pX - margin_px);
			} else if (close) {
				button.graphics.moveTo(margin_px, pX);
				button.graphics.lineTo(pX, margin_px);
				button.graphics.moveTo(margin_px, margin_px);
				button.graphics.lineTo(pX, pX);
			}
		}
	}
}

import ro.flashbite.interfaces.IDestroyable;

/**
 * @author Adrian Barbu
 * 
 * Base class to hold data to send throught binding calls
 */
class BindVO implements IDestroyable
{
	public var func:Function;
	public var params:Array;
	
	public function BindVO(func:Function, params:Array)
	{
		this.func = func;
		this.params = params;
	}
	
	public function destroy():void
	{
		func = null;
		params = null;
	}
}	